<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
        "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
    <link href="../Styles/sure3.css" rel="stylesheet" type="text/css"/>
    <link href="../Styles/base1.css" rel="stylesheet" type="text/css"/>
</head>

<body>
<div class="markdown-body entry-content" itemprop="text">
    <h1><a href="#module-的加载实现"  class="anchor" id="user-content-module-的加载实现">

    </a>Module 的加载实现
    </h1>
    <p>上一章介绍了模块的语法，本章介绍如何在浏览器和 Node 之中加载 ES6 模块，以及实际开发中经常遇到的一些问题（比如循环加载）。</p>
    <h2><a href="#浏览器加载"  class="anchor" id="user-content-浏览器加载">

    </a>浏览器加载
    </h2>
    <h3><a href="#传统方法"  class="anchor" id="user-content-传统方法">

    </a>传统方法
    </h3>
    <p>HTML 网页中，浏览器通过<code>&lt;script&gt;</code>标签加载 JavaScript 脚本。</p>
    <div class="highlight highlight-text-html-basic"><pre><span class="pl-c"><span
            class="pl-c">&lt;!--</span> 页面内嵌的脚本 <span class="pl-c">--&gt;</span></span>
&lt;<span class="pl-ent">script</span> <span class="pl-e">type</span>=<span class="pl-s"><span class="pl-pds">"</span>application/javascript<span
                class="pl-pds">"</span></span>&gt;<span class="pl-s1"></span>
<span class="pl-s1">  <span class="pl-c"><span class="pl-c">//</span> module code</span></span>
<span class="pl-s1"></span><span class="pl-s1">&lt;</span>/<span class="pl-ent">script</span>&gt;

<span class="pl-c"><span class="pl-c">&lt;!--</span> 外部脚本 <span class="pl-c">--&gt;</span></span>
&lt;<span class="pl-ent">script</span> <span class="pl-e">type</span>=<span class="pl-s"><span class="pl-pds">"</span>application/javascript<span
                class="pl-pds">"</span></span> <span class="pl-e">src</span>=<span class="pl-s"><span
                class="pl-pds">"</span>path/to/myModule.js<span class="pl-pds">"</span></span>&gt;<span
                class="pl-s1"></span>
<span class="pl-s1"></span><span class="pl-s1">&lt;</span>/<span class="pl-ent">script</span>&gt;</pre>
    </div>
    <p>上面代码中，由于浏览器脚本的默认语言是 JavaScript，因此<code>type="application/javascript"</code>可以省略。</p>
    <p>默认情况下，浏览器是同步加载 JavaScript 脚本，即渲染引擎遇到<code>&lt;script&gt;</code>标签就会停下来，等到执行完脚本，再继续向下渲染。如果是外部脚本，还必须加入脚本下载的时间。</p>
    <p>如果脚本体积很大，下载和执行的时间就会很长，因此造成浏览器堵塞，用户会感觉到浏览器“卡死”了，没有任何响应。这显然是很不好的体验，所以浏览器允许脚本异步加载，下面就是两种异步加载的语法。</p>
    <div class="highlight highlight-text-html-basic"><pre>&lt;<span class="pl-ent">script</span> <span
            class="pl-e">src</span>=<span class="pl-s"><span class="pl-pds">"</span>path/to/myModule.js<span
            class="pl-pds">"</span></span> <span class="pl-e">defer</span>&gt;&lt;/<span class="pl-ent">script</span>&gt;
&lt;<span class="pl-ent">script</span> <span class="pl-e">src</span>=<span class="pl-s"><span class="pl-pds">"</span>path/to/myModule.js<span
                class="pl-pds">"</span></span> <span class="pl-e">async</span>&gt;&lt;/<span
                class="pl-ent">script</span>&gt;</pre>
    </div>
    <p>上面代码中，<code>&lt;script&gt;</code>标签打开<code>defer</code>或<code>async</code>属性，脚本就会异步加载。渲染引擎遇到这一行命令，就会开始下载外部脚本，但不会等它下载和执行，而是直接执行后面的命令。
    </p>
    <p><code>defer</code>与<code>async</code>的区别是：<code>defer</code>要等到整个页面在内存中正常渲染结束（DOM 结构完全生成，以及其他脚本执行完成），才会执行；<code>async</code>一旦下载完，渲染引擎就会中断渲染，执行这个脚本以后，再继续渲染。一句话，<code>defer</code>是“渲染完再执行”，<code>async</code>是“下载完就执行”。另外，如果有多个<code>defer</code>脚本，会按照它们在页面出现的顺序加载，而多个<code>async</code>脚本是不能保证加载顺序的。
    </p>
    <h3><a href="#加载规则"  class="anchor" id="user-content-加载规则">

    </a>加载规则
    </h3>
    <p>浏览器加载 ES6 模块，也使用<code>&lt;script&gt;</code>标签，但是要加入<code>type="module"</code>属性。</p>
    <div class="highlight highlight-text-html-basic">
        <pre>&lt;<span class="pl-ent">script</span> <span class="pl-e">type</span>=<span class="pl-s"><span
                class="pl-pds">"</span>module<span class="pl-pds">"</span></span> <span class="pl-e">src</span>=<span
                class="pl-s"><span class="pl-pds">"</span>./foo.js<span class="pl-pds">"</span></span>&gt;&lt;/<span
                class="pl-ent">script</span>&gt;</pre>
    </div>
    <p>上面代码在网页中插入一个模块<code>foo.js</code>，由于<code>type</code>属性设为<code>module</code>，所以浏览器知道这是一个 ES6 模块。</p>
    <p>浏览器对于带有<code>type="module"</code>的<code>&lt;script&gt;</code>，都是异步加载，不会造成堵塞浏览器，即等到整个页面渲染完，再执行模块脚本，等同于打开了<code>
        &lt;script&gt;</code>标签的<code>defer</code>属性。</p>
    <div class="highlight highlight-text-html-basic"><pre>&lt;<span class="pl-ent">script</span> <span
            class="pl-e">type</span>=<span class="pl-s"><span class="pl-pds">"</span>module<span class="pl-pds">"</span></span> <span
            class="pl-e">src</span>=<span class="pl-s"><span class="pl-pds">"</span>./foo.js<span
            class="pl-pds">"</span></span>&gt;&lt;/<span class="pl-ent">script</span>&gt;
<span class="pl-c"><span class="pl-c">&lt;!--</span> 等同于 <span class="pl-c">--&gt;</span></span>
&lt;<span class="pl-ent">script</span> <span class="pl-e">type</span>=<span class="pl-s"><span class="pl-pds">"</span>module<span
                class="pl-pds">"</span></span> <span class="pl-e">src</span>=<span class="pl-s"><span
                class="pl-pds">"</span>./foo.js<span class="pl-pds">"</span></span> <span class="pl-e">defer</span>&gt;&lt;/<span
                class="pl-ent">script</span>&gt;</pre>
    </div>
    <p>如果网页有多个<code>&lt;script type="module"&gt;</code>，它们会按照在页面出现的顺序依次执行。</p>
    <p><code>&lt;script&gt;</code>标签的<code>async</code>属性也可以打开，这时只要加载完成，渲染引擎就会中断渲染立即执行。执行完成后，再恢复渲染。</p>
    <div class="highlight highlight-text-html-basic">
        <pre>&lt;<span class="pl-ent">script</span> <span class="pl-e">type</span>=<span class="pl-s"><span
                class="pl-pds">"</span>module<span class="pl-pds">"</span></span> <span class="pl-e">src</span>=<span
                class="pl-s"><span class="pl-pds">"</span>./foo.js<span class="pl-pds">"</span></span> <span
                class="pl-e">async</span>&gt;&lt;/<span class="pl-ent">script</span>&gt;</pre>
    </div>
    <p>一旦使用了<code>async</code>属性，<code>&lt;script type="module"&gt;</code>就不会按照在页面出现的顺序执行，而是只要该模块加载完成，就执行该模块。</p>
    <p>ES6 模块也允许内嵌在网页中，语法行为与加载外部脚本完全一致。</p>
    <div class="highlight highlight-text-html-basic"><pre>&lt;<span class="pl-ent">script</span> <span
            class="pl-e">type</span>=<span class="pl-s"><span class="pl-pds">"</span>module<span class="pl-pds">"</span></span>&gt;<span
            class="pl-s1"></span>
<span class="pl-s1">  <span class="pl-k">import</span> <span class="pl-smi">utils</span> <span class="pl-k">from</span> <span
        class="pl-s"><span class="pl-pds">"</span>./utils.js<span class="pl-pds">"</span></span>;</span>
<span class="pl-s1"></span>
<span class="pl-s1">  <span class="pl-c"><span class="pl-c">//</span> other code</span></span>
<span class="pl-s1"></span><span class="pl-s1">&lt;</span>/<span class="pl-ent">script</span>&gt;</pre>
    </div>
    <p>对于外部的模块脚本（上例是<code>foo.js</code>），有几点需要注意。</p>
    <ul>
        <li>代码是在模块作用域之中运行，而不是在全局作用域运行。模块内部的顶层变量，外部不可见。</li>
        <li>模块脚本自动采用严格模式，不管有没有声明<code>use strict</code>。</li>
        <li>模块之中，可以使用<code>import</code>命令加载其他模块（<code>.js</code>后缀不可省略，需要提供绝对 URL 或相对 URL），也可以使用<code>export</code>命令输出对外接口。
        </li>
        <li>模块之中，顶层的<code>this</code>关键字返回<code>undefined</code>，而不是指向<code>window</code>。也就是说，在模块顶层使用<code>this</code>关键字，是无意义的。
        </li>
        <li>同一个模块如果加载多次，将只执行一次。</li>
    </ul>
    <p>下面是一个示例模块。</p>
    <div class="highlight highlight-source-js"><pre><span class="pl-k">import</span> <span
            class="pl-smi">utils</span> <span class="pl-k">from</span> <span class="pl-s"><span class="pl-pds">'</span>https://example.com/js/utils.js<span
            class="pl-pds">'</span></span>;

<span class="pl-k">const</span> <span class="pl-c1">x</span> <span class="pl-k">=</span> <span class="pl-c1">1</span>;

<span class="pl-en">console</span>.<span class="pl-c1">log</span>(x <span class="pl-k">===</span> <span class="pl-c1">window</span>.<span
                class="pl-c1">x</span>); <span class="pl-c"><span class="pl-c">//</span>false</span>
<span class="pl-en">console</span>.<span class="pl-c1">log</span>(<span class="pl-c1">this</span> <span
                class="pl-k">===</span> <span class="pl-c1">undefined</span>); <span class="pl-c"><span
                class="pl-c">//</span> true</span>

<span class="pl-k">delete</span> x; <span class="pl-c"><span class="pl-c">//</span> 句法错误，严格模式禁止删除变量</span></pre>
    </div>
    <p>利用顶层的<code>this</code>等于<code>undefined</code>这个语法点，可以侦测当前代码是否在 ES6 模块之中。</p>
    <div class="highlight highlight-source-js">
        <pre><span class="pl-k">const</span> <span class="pl-c1">isNotModuleScript</span> <span
                class="pl-k">=</span> <span class="pl-c1">this</span> <span class="pl-k">!==</span> <span class="pl-c1">undefined</span>;</pre>
    </div>
    <h2><a href="#es6-模块与-commonjs-模块的差异"  class="anchor" id="user-content-es6-模块与-commonjs-模块的差异">

    </a>ES6 模块与 CommonJS 模块的差异
    </h2>
    <p>讨论 Node 加载 ES6 模块之前，必须了解 ES6 模块与 CommonJS 模块完全不同。</p>
    <p>它们有两个重大差异。</p>
    <ul>
        <li>CommonJS 模块输出的是一个值的拷贝，ES6 模块输出的是值的引用。</li>
        <li>CommonJS 模块是运行时加载，ES6 模块是编译时输出接口。</li>
    </ul>
    <p>第二个差异是因为 CommonJS 加载的是一个对象（即<code>module.exports</code>属性），该对象只有在脚本运行完才会生成。而 ES6
        模块不是对象，它的对外接口只是一种静态定义，在代码静态解析阶段就会生成。</p>
    <p>下面重点解释第一个差异。</p>
    <p>CommonJS 模块输出的是值的拷贝，也就是说，一旦输出一个值，模块内部的变化就影响不到这个值。请看下面这个模块文件<code>lib.js</code>的例子。</p>
    <div class="highlight highlight-source-js"><pre><span class="pl-c"><span class="pl-c">//</span> lib.js</span>
<span class="pl-k">var</span> counter <span class="pl-k">=</span> <span class="pl-c1">3</span>;
<span class="pl-k">function</span> <span class="pl-en">incCounter</span>() {
  counter<span class="pl-k">++</span>;
}
<span class="pl-c1">module</span>.<span class="pl-smi">exports</span> <span class="pl-k">=</span> {
  counter<span class="pl-k">:</span> counter,
  incCounter<span class="pl-k">:</span> incCounter,
};</pre>
    </div>
    <p>上面代码输出内部变量<code>counter</code>和改写这个变量的内部方法<code>incCounter</code>。然后，在<code>main.js</code>里面加载这个模块。</p>
    <div class="highlight highlight-source-js"><pre><span class="pl-c"><span class="pl-c">//</span> main.js</span>
<span class="pl-k">var</span> mod <span class="pl-k">=</span> <span class="pl-c1">require</span>(<span
                class="pl-s"><span class="pl-pds">'</span>./lib<span class="pl-pds">'</span></span>);

<span class="pl-en">console</span>.<span class="pl-c1">log</span>(<span class="pl-smi">mod</span>.<span class="pl-smi">counter</span>);  <span
                class="pl-c"><span class="pl-c">//</span> 3</span>
<span class="pl-smi">mod</span>.<span class="pl-en">incCounter</span>();
<span class="pl-en">console</span>.<span class="pl-c1">log</span>(<span class="pl-smi">mod</span>.<span class="pl-smi">counter</span>); <span
                class="pl-c"><span class="pl-c">//</span> 3</span></pre>
    </div>
    <p>上面代码说明，<code>lib.js</code>模块加载以后，它的内部变化就影响不到输出的<code>mod.counter</code>了。这是因为<code>mod.counter</code>是一个原始类型的值，会被缓存。除非写成一个函数，才能得到内部变动后的值。
    </p>
    <div class="highlight highlight-source-js"><pre><span class="pl-c"><span class="pl-c">//</span> lib.js</span>
<span class="pl-k">var</span> counter <span class="pl-k">=</span> <span class="pl-c1">3</span>;
<span class="pl-k">function</span> <span class="pl-en">incCounter</span>() {
  counter<span class="pl-k">++</span>;
}
<span class="pl-c1">module</span>.<span class="pl-smi">exports</span> <span class="pl-k">=</span> {
  <span class="pl-k">get</span> <span class="pl-en">counter</span>() {
    <span class="pl-k">return</span> counter
  },
  incCounter<span class="pl-k">:</span> incCounter,
};</pre>
    </div>
    <p>上面代码中，输出的<code>counter</code>属性实际上是一个取值器函数。现在再执行<code>main.js</code>，就可以正确读取内部变量<code>counter</code>的变动了。</p>
    <div class="highlight highlight-source-shell"><pre>$ node main.js
3
4</pre>
    </div>
    <p>ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候，遇到模块加载命令<code>import</code>，就会生成一个只读引用。等到脚本真正执行时，再根据这个只读引用，到被加载的那个模块里面去取值。换句话说，ES6
        的<code>import</code>有点像 Unix 系统的“符号连接”，原始值变了，<code>import</code>加载的值也会跟着变。因此，ES6
        模块是动态引用，并且不会缓存值，模块里面的变量绑定其所在的模块。</p>
    <p>还是举上面的例子。</p>
    <div class="highlight highlight-source-js"><pre><span class="pl-c"><span class="pl-c">//</span> lib.js</span>
<span class="pl-k">export</span> <span class="pl-k">let</span> counter <span class="pl-k">=</span> <span
                class="pl-c1">3</span>;
<span class="pl-k">export</span> <span class="pl-k">function</span> <span class="pl-en">incCounter</span>() {
  counter<span class="pl-k">++</span>;
}

<span class="pl-c"><span class="pl-c">//</span> main.js</span>
<span class="pl-k">import</span> { <span class="pl-smi">counter</span>, <span class="pl-smi">incCounter</span> } <span
                class="pl-k">from</span> <span class="pl-s"><span class="pl-pds">'</span>./lib<span
                class="pl-pds">'</span></span>;
<span class="pl-en">console</span>.<span class="pl-c1">log</span>(counter); <span class="pl-c"><span
                class="pl-c">//</span> 3</span>
<span class="pl-en">incCounter</span>();
<span class="pl-en">console</span>.<span class="pl-c1">log</span>(counter); <span class="pl-c"><span
                class="pl-c">//</span> 4</span></pre>
    </div>
    <p>上面代码说明，ES6 模块输入的变量<code>counter</code>是活的，完全反应其所在模块<code>lib.js</code>内部的变化。</p>
    <p>再举一个出现在<code>export</code>一节中的例子。</p>
    <div class="highlight highlight-source-js"><pre><span class="pl-c"><span class="pl-c">//</span> m1.js</span>
<span class="pl-k">export</span> <span class="pl-k">var</span> foo <span class="pl-k">=</span> <span class="pl-s"><span
                class="pl-pds">'</span>bar<span class="pl-pds">'</span></span>;
<span class="pl-c1">setTimeout</span>(() <span class="pl-k">=&gt;</span> foo <span class="pl-k">=</span> <span
                class="pl-s"><span class="pl-pds">'</span>baz<span class="pl-pds">'</span></span>, <span class="pl-c1">500</span>);

<span class="pl-c"><span class="pl-c">//</span> m2.js</span>
<span class="pl-k">import</span> {<span class="pl-smi">foo</span>} <span class="pl-k">from</span> <span
                class="pl-s"><span class="pl-pds">'</span>./m1.js<span class="pl-pds">'</span></span>;
<span class="pl-en">console</span>.<span class="pl-c1">log</span>(foo);
<span class="pl-c1">setTimeout</span>(() <span class="pl-k">=&gt;</span> <span class="pl-en">console</span>.<span
                class="pl-c1">log</span>(foo), <span class="pl-c1">500</span>);</pre>
    </div>
    <p>上面代码中，<code>m1.js</code>的变量<code>foo</code>，在刚加载时等于<code>bar</code>，过了 500 毫秒，又变为等于<code>baz</code>。</p>
    <p>让我们看看，<code>m2.js</code>能否正确读取这个变化。</p>
    <div class="highlight highlight-source-shell"><pre>$ babel-node m2.js

bar
baz</pre>
    </div>
    <p>上面代码表明，ES6 模块不会缓存运行结果，而是动态地去被加载的模块取值，并且变量总是绑定其所在的模块。</p>
    <p>由于 ES6 输入的模块变量，只是一个“符号连接”，所以这个变量是只读的，对它进行重新赋值会报错。</p>
    <div class="highlight highlight-source-js"><pre><span class="pl-c"><span class="pl-c">//</span> lib.js</span>
<span class="pl-k">export</span> <span class="pl-k">let</span> obj <span class="pl-k">=</span> {};

<span class="pl-c"><span class="pl-c">//</span> main.js</span>
<span class="pl-k">import</span> { <span class="pl-smi">obj</span> } <span class="pl-k">from</span> <span
                class="pl-s"><span class="pl-pds">'</span>./lib<span class="pl-pds">'</span></span>;

<span class="pl-smi">obj</span>.<span class="pl-smi">prop</span> <span class="pl-k">=</span> <span
                class="pl-c1">123</span>; <span class="pl-c"><span class="pl-c">//</span> OK</span>
obj <span class="pl-k">=</span> {}; <span class="pl-c"><span class="pl-c">//</span> TypeError</span></pre>
    </div>
    <p>上面代码中，<code>main.js</code>从<code>lib.js</code>输入变量<code>obj</code>，可以对<code>obj</code>添加属性，但是重新赋值就会报错。因为变量<code>obj</code>指向的地址是只读的，不能重新赋值，这就好比<code>main.js</code>创造了一个名为<code>obj</code>的<code>const</code>变量。
    </p>
    <p>最后，<code>export</code>通过接口，输出的是同一个值。不同的脚本加载这个接口，得到的都是同样的实例。</p>
    <div class="highlight highlight-source-js"><pre><span class="pl-c"><span class="pl-c">//</span> mod.js</span>
<span class="pl-k">function</span> <span class="pl-en">C</span>() {
  <span class="pl-c1">this</span>.<span class="pl-smi">sum</span> <span class="pl-k">=</span> <span
                class="pl-c1">0</span>;
  <span class="pl-c1">this</span>.<span class="pl-en">add</span> <span class="pl-k">=</span> <span
                class="pl-k">function</span> () {
    <span class="pl-c1">this</span>.<span class="pl-smi">sum</span> <span class="pl-k">+=</span> <span
                class="pl-c1">1</span>;
  };
  <span class="pl-c1">this</span>.<span class="pl-en">show</span> <span class="pl-k">=</span> <span class="pl-k">function</span> () {
    <span class="pl-en">console</span>.<span class="pl-c1">log</span>(<span class="pl-c1">this</span>.<span
                class="pl-smi">sum</span>);
  };
}

<span class="pl-k">export</span> <span class="pl-k">let</span> c <span class="pl-k">=</span> <span
                class="pl-k">new</span> <span class="pl-en">C</span>();</pre>
    </div>
    <p>上面的脚本<code>mod.js</code>，输出的是一个<code>C</code>的实例。不同的脚本加载这个模块，得到的都是同一个实例。</p>
    <div class="highlight highlight-source-js"><pre><span class="pl-c"><span class="pl-c">//</span> x.js</span>
<span class="pl-k">import</span> {<span class="pl-smi">c</span>} <span class="pl-k">from</span> <span class="pl-s"><span
                class="pl-pds">'</span>./mod<span class="pl-pds">'</span></span>;
<span class="pl-smi">c</span>.<span class="pl-c1">add</span>();

<span class="pl-c"><span class="pl-c">//</span> y.js</span>
<span class="pl-k">import</span> {<span class="pl-smi">c</span>} <span class="pl-k">from</span> <span class="pl-s"><span
                class="pl-pds">'</span>./mod<span class="pl-pds">'</span></span>;
<span class="pl-smi">c</span>.<span class="pl-en">show</span>();

<span class="pl-c"><span class="pl-c">//</span> main.js</span>
<span class="pl-k">import</span> <span class="pl-s"><span class="pl-pds">'</span>./x<span class="pl-pds">'</span></span>;
<span class="pl-k">import</span> <span class="pl-s"><span class="pl-pds">'</span>./y<span class="pl-pds">'</span></span>;</pre>
    </div>
    <p>现在执行<code>main.js</code>，输出的是<code>1</code>。</p>
    <div class="highlight highlight-source-shell"><pre>$ babel-node main.js
1</pre>
    </div>
    <p>这就证明了<code>x.js</code>和<code>y.js</code>加载的都是<code>C</code>的同一个实例。</p>
    <h2><a href="#node-加载"  class="anchor" id="user-content-node-加载">

    </a>Node 加载
    </h2>
    <h3><a href="#概述"  class="anchor" id="user-content-概述">

    </a>概述
    </h3>
    <p>Node 对 ES6 模块的处理比较麻烦，因为它有自己的 CommonJS 模块格式，与 ES6 模块格式是不兼容的。目前的解决方案是，将两者分开，ES6 模块和 CommonJS 采用各自的加载方案。</p>
    <p>Node 要求 ES6 模块采用<code>.mjs</code>后缀文件名。也就是说，只要脚本文件里面使用<code>import</code>或者<code>export</code>命令，那么就必须采用<code>.mjs</code>后缀名。<code>require</code>命令不能加载<code>.mjs</code>文件，会报错，只有<code>import</code>命令才可以加载<code>.mjs</code>文件。反过来，<code>.mjs</code>文件里面也不能使用<code>require</code>命令，必须使用<code>import</code>。
    </p>
    <p>目前，这项功能还在试验阶段。安装 Node v8.5.0 或以上版本，要用<code>--experimental-modules</code>参数才能打开该功能。</p>
    <div class="highlight highlight-source-shell">
        <pre>$ node --experimental-modules my-app.mjs</pre>
    </div>
    <p>为了与浏览器的<code>import</code>加载规则相同，Node 的<code>.mjs</code>文件支持 URL 路径。</p>
    <div class="highlight highlight-source-js">
        <pre><span class="pl-k">import</span> <span class="pl-s"><span class="pl-pds">'</span>./foo?query=1<span
                class="pl-pds">'</span></span>; <span class="pl-c"><span
                class="pl-c">//</span> 加载 ./foo 传入参数 ?query=1</span></pre>
    </div>
    <p>上面代码中，脚本路径带有参数<code>?query=1</code>，Node 会按 URL 规则解读。同一个脚本只要参数不同，就会被加载多次，并且保存成不同的缓存。由于这个原因，只要文件名中含有<code>:</code>、<code>%</code>、<code>#</code>、<code>?</code>等特殊字符，最好对这些字符进行转义。
    </p>
    <p>目前，Node 的<code>import</code>命令只支持加载本地模块（<code>file:</code>协议），不支持加载远程模块。</p>
    <p>如果模块名不含路径，那么<code>import</code>命令会去<code>node_modules</code>目录寻找这个模块。</p>
    <div class="highlight highlight-source-js"><pre><span class="pl-k">import</span> <span class="pl-s"><span
            class="pl-pds">'</span>baz<span class="pl-pds">'</span></span>;
<span class="pl-k">import</span> <span class="pl-s"><span class="pl-pds">'</span>abc/123<span
                class="pl-pds">'</span></span>;</pre>
    </div>
    <p>如果模块名包含路径，那么<code>import</code>命令会按照路径去寻找这个名字的脚本文件。</p>
    <div class="highlight highlight-source-js"><pre><span class="pl-k">import</span> <span class="pl-s"><span
            class="pl-pds">'</span>file:///etc/config/app.json<span class="pl-pds">'</span></span>;
<span class="pl-k">import</span> <span class="pl-s"><span class="pl-pds">'</span>./foo<span
                class="pl-pds">'</span></span>;
<span class="pl-k">import</span> <span class="pl-s"><span class="pl-pds">'</span>./foo?search<span
                class="pl-pds">'</span></span>;
<span class="pl-k">import</span> <span class="pl-s"><span class="pl-pds">'</span>../bar<span
                class="pl-pds">'</span></span>;
<span class="pl-k">import</span> <span class="pl-s"><span class="pl-pds">'</span>/baz<span
                class="pl-pds">'</span></span>;</pre>
    </div>
    <p>如果脚本文件省略了后缀名，比如<code>import './foo'</code>，Node 会依次尝试四个后缀名：<code>./foo.mjs</code>、<code>./foo.js</code>、<code>./foo.json</code>、<code>./foo.node</code>。如果这些脚本文件都不存在，Node
        就会去加载<code>./foo/package.json</code>的<code>main</code>字段指定的脚本。如果<code>./foo/package.json</code>不存在或者没有<code>main</code>字段，那么就会依次加载<code>./foo/index.mjs</code>、<code>./foo/index.js</code>、<code>./foo/index.json</code>、<code>./foo/index.node</code>。如果以上四个文件还是都不存在，就会抛出错误。
    </p>
    <p>最后，Node 的<code>import</code>命令是异步加载，这一点与浏览器的处理方法相同。</p>
    <h3><a href="#内部变量"  class="anchor" id="user-content-内部变量">

    </a>内部变量
    </h3>
    <p>ES6 模块应该是通用的，同一个模块不用修改，就可以用在浏览器环境和服务器环境。为了达到这个目标，Node 规定 ES6 模块之中不能使用 CommonJS 模块的特有的一些内部变量。</p>
    <p>首先，就是<code>this</code>关键字。ES6 模块之中，顶层的<code>this</code>指向<code>undefined</code>；CommonJS 模块的顶层<code>this</code>指向当前模块，这是两者的一个重大差异。
    </p>
    <p>其次，以下这些顶层变量在 ES6 模块之中都是不存在的。</p>
    <ul>
        <li><code>arguments</code></li>
        <li><code>require</code></li>
        <li><code>module</code></li>
        <li><code>exports</code></li>
        <li><code>__filename</code></li>
        <li><code>__dirname</code></li>
    </ul>
    <p>如果你一定要使用这些变量，有一个变通方法，就是写一个 CommonJS 模块输出这些变量，然后再用 ES6 模块加载这个 CommonJS 模块。但是这样一来，该 ES6
        模块就不能直接用于浏览器环境了，所以不推荐这样做。</p>
    <div class="highlight highlight-source-js"><pre><span class="pl-c"><span class="pl-c">//</span> expose.js</span>
<span class="pl-c1">module</span>.<span class="pl-smi">exports</span> <span class="pl-k">=</span> {<span class="pl-c1">__dirname</span>};

<span class="pl-c"><span class="pl-c">//</span> use.mjs</span>
<span class="pl-k">import</span> <span class="pl-smi">expose</span> <span class="pl-k">from</span> <span
                class="pl-s"><span class="pl-pds">'</span>./expose.js<span class="pl-pds">'</span></span>;
<span class="pl-k">const</span> {<span class="pl-c1">__dirname</span>} <span class="pl-k">=</span> expose;</pre>
    </div>
    <p>上面代码中，<code>expose.js</code>是一个 CommonJS 模块，输出变量<code>__dirname</code>，该变量在 ES6 模块之中不存在。ES6
        模块加载<code>expose.js</code>，就可以得到<code>__dirname</code>。</p>
    <h3><a href="#es6-模块加载-commonjs-模块"  class="anchor" id="user-content-es6-模块加载-commonjs-模块">

    </a>ES6 模块加载 CommonJS 模块
    </h3>
    <p>CommonJS 模块的输出都定义在<code>module.exports</code>这个属性上面。Node 的<code>import</code>命令加载 CommonJS 模块，Node 会自动将<code>module.exports</code>属性，当作模块的默认输出，即等同于<code>export
        default xxx</code>。</p>
    <p>下面是一个 CommonJS 模块。</p>
    <div class="highlight highlight-source-js"><pre><span class="pl-c"><span class="pl-c">//</span> a.js</span>
<span class="pl-c1">module</span>.<span class="pl-smi">exports</span> <span class="pl-k">=</span> {
  foo<span class="pl-k">:</span> <span class="pl-s"><span class="pl-pds">'</span>hello<span
                class="pl-pds">'</span></span>,
  bar<span class="pl-k">:</span> <span class="pl-s"><span class="pl-pds">'</span>world<span
                class="pl-pds">'</span></span>
};

<span class="pl-c"><span class="pl-c">//</span> 等同于</span>
<span class="pl-k">export</span> <span class="pl-c1">default</span> {
  foo<span class="pl-k">:</span> <span class="pl-s"><span class="pl-pds">'</span>hello<span
                class="pl-pds">'</span></span>,
  bar<span class="pl-k">:</span> <span class="pl-s"><span class="pl-pds">'</span>world<span
                class="pl-pds">'</span></span>
};</pre>
    </div>
    <p><code>import</code>命令加载上面的模块，<code>module.exports</code>会被视为默认输出，即<code>import</code>命令实际上输入的是这样一个对象<code>{
        default: module.exports }</code>。</p>
    <p>所以，一共有三种写法，可以拿到 CommonJS 模块的<code>module.exports</code>。</p>
    <div class="highlight highlight-source-js"><pre><span class="pl-c"><span class="pl-c">//</span> 写法一</span>
<span class="pl-k">import</span> <span class="pl-smi">baz</span> <span class="pl-k">from</span> <span class="pl-s"><span
                class="pl-pds">'</span>./a<span class="pl-pds">'</span></span>;
<span class="pl-c"><span class="pl-c">//</span> baz = {foo: 'hello', bar: 'world'};</span>

<span class="pl-c"><span class="pl-c">//</span> 写法二</span>
<span class="pl-k">import</span> {<span class="pl-c1">default</span> <span class="pl-k">as</span> <span class="pl-smi">baz</span>} <span
                class="pl-k">from</span> <span class="pl-s"><span class="pl-pds">'</span>./a<span
                class="pl-pds">'</span></span>;
<span class="pl-c"><span class="pl-c">//</span> baz = {foo: 'hello', bar: 'world'};</span>

<span class="pl-c"><span class="pl-c">//</span> 写法三</span>
<span class="pl-k">import</span> <span class="pl-c1">*</span> <span class="pl-k">as</span> <span
                class="pl-smi">baz</span> <span class="pl-k">from</span> <span class="pl-s"><span
                class="pl-pds">'</span>./a<span class="pl-pds">'</span></span>;
<span class="pl-c"><span class="pl-c">//</span> baz = {</span>
<span class="pl-c"><span class="pl-c">//</span>   get default() {return module.exports;},</span>
<span class="pl-c"><span class="pl-c">//</span>   get foo() {return this.default.foo}.bind(baz),</span>
<span class="pl-c"><span class="pl-c">//</span>   get bar() {return this.default.bar}.bind(baz)</span>
<span class="pl-c"><span class="pl-c">//</span> }</span></pre>
    </div>
    <p>上面代码的第三种写法，可以通过<code>baz.default</code>拿到<code>module.exports</code>。<code>foo</code>属性和<code>bar</code>属性就是可以通过这种方法拿到了<code>module.exports</code>。
    </p>
    <p>下面是一些例子。</p>
    <div class="highlight highlight-source-js"><pre><span class="pl-c"><span class="pl-c">//</span> b.js</span>
<span class="pl-c1">module</span>.<span class="pl-smi">exports</span> <span class="pl-k">=</span> <span class="pl-c1">null</span>;

<span class="pl-c"><span class="pl-c">//</span> es.js</span>
<span class="pl-k">import</span> <span class="pl-smi">foo</span> <span class="pl-k">from</span> <span class="pl-s"><span
                class="pl-pds">'</span>./b<span class="pl-pds">'</span></span>;
<span class="pl-c"><span class="pl-c">//</span> foo = null;</span>

<span class="pl-k">import</span> <span class="pl-c1">*</span> <span class="pl-k">as</span> <span
                class="pl-smi">bar</span> <span class="pl-k">from</span> <span class="pl-s"><span
                class="pl-pds">'</span>./b<span class="pl-pds">'</span></span>;
<span class="pl-c"><span class="pl-c">//</span> bar = { default:null };</span></pre>
    </div>
    <p>上面代码中，<code>es.js</code>采用第二种写法时，要通过<code>bar.default</code>这样的写法，才能拿到<code>module.exports</code>。</p>
    <div class="highlight highlight-source-js"><pre><span class="pl-c"><span class="pl-c">//</span> c.js</span>
<span class="pl-c1">module</span>.<span class="pl-en">exports</span> <span class="pl-k">=</span> <span class="pl-k">function</span> <span
                class="pl-en">two</span>() {
  <span class="pl-k">return</span> <span class="pl-c1">2</span>;
};

<span class="pl-c"><span class="pl-c">//</span> es.js</span>
<span class="pl-k">import</span> <span class="pl-smi">foo</span> <span class="pl-k">from</span> <span class="pl-s"><span
                class="pl-pds">'</span>./c<span class="pl-pds">'</span></span>;
<span class="pl-en">foo</span>(); <span class="pl-c"><span class="pl-c">//</span> 2</span>

<span class="pl-k">import</span> <span class="pl-c1">*</span> <span class="pl-k">as</span> <span
                class="pl-smi">bar</span> <span class="pl-k">from</span> <span class="pl-s"><span
                class="pl-pds">'</span>./c<span class="pl-pds">'</span></span>;
<span class="pl-smi">bar</span>.<span class="pl-en">default</span>(); <span class="pl-c"><span class="pl-c">//</span> 2</span>
<span class="pl-en">bar</span>(); <span class="pl-c"><span
                class="pl-c">//</span> throws, bar is not a function</span></pre>
    </div>
    <p>上面代码中，<code>bar</code>本身是一个对象，不能当作函数调用，只能通过<code>bar.default</code>调用。</p>
    <p>CommonJS 模块的输出缓存机制，在 ES6 加载方式下依然有效。</p>
    <div class="highlight highlight-source-js"><pre><span class="pl-c"><span class="pl-c">//</span> foo.js</span>
<span class="pl-c1">module</span>.<span class="pl-smi">exports</span> <span class="pl-k">=</span> <span class="pl-c1">123</span>;
<span class="pl-c1">setTimeout</span>(<span class="pl-smi">_</span> <span class="pl-k">=&gt;</span> <span class="pl-c1">module</span>.<span
                class="pl-smi">exports</span> <span class="pl-k">=</span> <span class="pl-c1">null</span>);</pre>
    </div>
    <p>上面代码中，对于加载<code>foo.js</code>的脚本，<code>module.exports</code>将一直是<code>123</code>，而不会变成<code>null</code>。</p>
    <p>由于 ES6 模块是编译时确定输出接口，CommonJS 模块是运行时确定输出接口，所以采用<code>import</code>命令加载 CommonJS 模块时，不允许采用下面的写法。</p>
    <div class="highlight highlight-source-js"><pre><span class="pl-c"><span class="pl-c">//</span> 不正确</span>
<span class="pl-k">import</span> { <span class="pl-smi">readfile</span> } <span class="pl-k">from</span> <span
                class="pl-s"><span class="pl-pds">'</span>fs<span class="pl-pds">'</span></span>;</pre>
    </div>
    <p>上面的写法不正确，因为<code>fs</code>是 CommonJS 格式，只有在运行时才能确定<code>readfile</code>接口，而<code>import</code>命令要求编译时就确定这个接口。解决方法就是改为整体输入。
    </p>
    <div class="highlight highlight-source-js"><pre><span class="pl-c"><span class="pl-c">//</span> 正确的写法一</span>
<span class="pl-k">import</span> <span class="pl-c1">*</span> <span class="pl-k">as</span> <span
                class="pl-smi">express</span> <span class="pl-k">from</span> <span class="pl-s"><span
                class="pl-pds">'</span>express<span class="pl-pds">'</span></span>;
<span class="pl-k">const</span> <span class="pl-c1">app</span> <span class="pl-k">=</span> <span
                class="pl-smi">express</span>.<span class="pl-en">default</span>();

<span class="pl-c"><span class="pl-c">//</span> 正确的写法二</span>
<span class="pl-k">import</span> <span class="pl-smi">express</span> <span class="pl-k">from</span> <span
                class="pl-s"><span class="pl-pds">'</span>express<span class="pl-pds">'</span></span>;
<span class="pl-k">const</span> <span class="pl-c1">app</span> <span class="pl-k">=</span> <span
                class="pl-en">express</span>();</pre>
    </div>
    <h3><a href="#commonjs-模块加载-es6-模块"  class="anchor" id="user-content-commonjs-模块加载-es6-模块">

    </a>CommonJS 模块加载 ES6 模块
    </h3>
    <p>CommonJS 模块加载 ES6 模块，不能使用<code>require</code>命令，而要使用<code>import()</code>函数。ES6 模块的所有输出接口，会成为输入对象的属性。</p>
    <div class="highlight highlight-source-js"><pre><span class="pl-c"><span class="pl-c">//</span> es.mjs</span>
<span class="pl-k">let</span> foo <span class="pl-k">=</span> { bar<span class="pl-k">:</span> <span class="pl-s"><span
                class="pl-pds">'</span>my-default<span class="pl-pds">'</span></span> };
<span class="pl-k">export</span> <span class="pl-c1">default</span> <span class="pl-smi">foo</span>;
foo <span class="pl-k">=</span> <span class="pl-c1">null</span>;

<span class="pl-c"><span class="pl-c">//</span> cjs.js</span>
<span class="pl-k">const</span> <span class="pl-c1">es_namespace</span> <span class="pl-k">=</span> <span class="pl-k">await</span> <span
                class="pl-k">import</span>(<span class="pl-s"><span class="pl-pds">'</span>./es<span
                class="pl-pds">'</span></span>);
<span class="pl-c"><span class="pl-c">//</span> es_namespace = {</span>
<span class="pl-c"><span class="pl-c">//</span>   get default() {</span>
<span class="pl-c"><span class="pl-c">//</span>     ...</span>
<span class="pl-c"><span class="pl-c">//</span>   }</span>
<span class="pl-c"><span class="pl-c">//</span> }</span>
<span class="pl-en">console</span>.<span class="pl-c1">log</span>(<span class="pl-smi">es_namespace</span>.<span
                class="pl-smi">default</span>);
<span class="pl-c"><span class="pl-c">//</span> { bar:'my-default' }</span></pre>
    </div>
    <p>
        上面代码中，<code>default</code>接口变成了<code>es_namespace.default</code>属性。另外，由于存在缓存机制，<code>es.js</code>对<code>foo</code>的重新赋值没有在模块外部反映出来。
    </p>
    <p>下面是另一个例子。</p>
    <div class="highlight highlight-source-js"><pre><span class="pl-c"><span class="pl-c">//</span> es.js</span>
<span class="pl-k">export</span> <span class="pl-k">let</span> foo <span class="pl-k">=</span> { bar<span
                class="pl-k">:</span><span class="pl-s"><span class="pl-pds">'</span>my-default<span
                class="pl-pds">'</span></span> };
<span class="pl-k">export</span> { <span class="pl-smi">foo</span> <span class="pl-k">as</span> <span
                class="pl-smi">bar</span> };
<span class="pl-k">export</span> <span class="pl-k">function</span> <span class="pl-en">f</span>() {};
<span class="pl-k">export</span> <span class="pl-k">class</span> <span class="pl-en">c</span> {};

<span class="pl-c"><span class="pl-c">//</span> cjs.js</span>
<span class="pl-k">const</span> <span class="pl-c1">es_namespace</span> <span class="pl-k">=</span> <span class="pl-k">await</span> <span
                class="pl-k">import</span>(<span class="pl-s"><span class="pl-pds">'</span>./es<span
                class="pl-pds">'</span></span>);
<span class="pl-c"><span class="pl-c">//</span> es_namespace = {</span>
<span class="pl-c"><span class="pl-c">//</span>   get foo() {return foo;}</span>
<span class="pl-c"><span class="pl-c">//</span>   get bar() {return foo;}</span>
<span class="pl-c"><span class="pl-c">//</span>   get f() {return f;}</span>
<span class="pl-c"><span class="pl-c">//</span>   get c() {return c;}</span>
<span class="pl-c"><span class="pl-c">//</span> }</span></pre>
    </div>
    <h2><a href="#循环加载"  class="anchor" id="user-content-循环加载">

    </a>循环加载
    </h2>
    <p>“循环加载”（circular dependency）指的是，<code>a</code>脚本的执行依赖<code>b</code>脚本，而<code>b</code>脚本的执行又依赖<code>a</code>脚本。</p>
    <div class="highlight highlight-source-js"><pre><span class="pl-c"><span class="pl-c">//</span> a.js</span>
<span class="pl-k">var</span> b <span class="pl-k">=</span> <span class="pl-c1">require</span>(<span class="pl-s"><span
                class="pl-pds">'</span>b<span class="pl-pds">'</span></span>);

<span class="pl-c"><span class="pl-c">//</span> b.js</span>
<span class="pl-k">var</span> a <span class="pl-k">=</span> <span class="pl-c1">require</span>(<span class="pl-s"><span
                class="pl-pds">'</span>a<span class="pl-pds">'</span></span>);</pre>
    </div>
    <p>通常，“循环加载”表示存在强耦合，如果处理不好，还可能导致递归加载，使得程序无法执行，因此应该避免出现。</p>
    <p>
        但是实际上，这是很难避免的，尤其是依赖关系复杂的大项目，很容易出现<code>a</code>依赖<code>b</code>，<code>b</code>依赖<code>c</code>，<code>c</code>又依赖<code>a</code>这样的情况。这意味着，模块加载机制必须考虑“循环加载”的情况。
    </p>
    <p>对于 JavaScript 语言来说，目前最常见的两种模块格式 CommonJS 和 ES6，处理“循环加载”的方法是不一样的，返回的结果也不一样。</p>
    <h3><a href="#commonjs-模块的加载原理"  class="anchor" id="user-content-commonjs-模块的加载原理">

    </a>CommonJS 模块的加载原理
    </h3>
    <p>介绍 ES6 如何处理“循环加载”之前，先介绍目前最流行的 CommonJS 模块格式的加载原理。</p>
    <p>CommonJS 的一个模块，就是一个脚本文件。<code>require</code>命令第一次加载该脚本，就会执行整个脚本，然后在内存生成一个对象。</p>
    <div class="highlight highlight-source-js"><pre>{
  id<span class="pl-k">:</span> <span class="pl-s"><span class="pl-pds">'</span>...<span class="pl-pds">'</span></span>,
  exports<span class="pl-k">:</span> { <span class="pl-k">...</span> },
  loaded<span class="pl-k">:</span> <span class="pl-c1">true</span>,
  <span class="pl-k">...</span>
}</pre>
    </div>
    <p>上面代码就是 Node 内部加载模块后生成的一个对象。该对象的<code>id</code>属性是模块名，<code>exports</code>属性是模块输出的各个接口，<code>loaded</code>属性是一个布尔值，表示该模块的脚本是否执行完毕。其他还有很多属性，这里都省略了。
    </p>
    <p>以后需要用到这个模块的时候，就会到<code>exports</code>属性上面取值。即使再次执行<code>require</code>命令，也不会再次执行该模块，而是到缓存之中取值。也就是说，CommonJS
        模块无论加载多少次，都只会在第一次加载时运行一次，以后再加载，就返回第一次运行的结果，除非手动清除系统缓存。</p>
    <h3><a href="#commonjs-模块的循环加载"  class="anchor" id="user-content-commonjs-模块的循环加载">

    </a>CommonJS 模块的循环加载
    </h3>
    <p>CommonJS 模块的重要特性是加载时执行，即脚本代码在<code>require</code>的时候，就会全部执行。一旦出现某个模块被"循环加载"，就只输出已经执行的部分，还未执行的部分不会输出。</p>
    <p>让我们来看，Node <a href="https://nodejs.org/api/modules.html#modules_cycles" rel="nofollow">官方文档</a>里面的例子。脚本文件<code>a.js</code>代码如下。
    </p>
    <div class="highlight highlight-source-js"><pre><span class="pl-c1">exports</span>.<span class="pl-smi">done</span> <span
            class="pl-k">=</span> <span class="pl-c1">false</span>;
<span class="pl-k">var</span> b <span class="pl-k">=</span> <span class="pl-c1">require</span>(<span class="pl-s"><span
                class="pl-pds">'</span>./b.js<span class="pl-pds">'</span></span>);
<span class="pl-en">console</span>.<span class="pl-c1">log</span>(<span class="pl-s"><span class="pl-pds">'</span>在 a.js 之中，b.done = %j<span
                class="pl-pds">'</span></span>, <span class="pl-smi">b</span>.<span class="pl-smi">done</span>);
<span class="pl-c1">exports</span>.<span class="pl-smi">done</span> <span class="pl-k">=</span> <span
                class="pl-c1">true</span>;
<span class="pl-en">console</span>.<span class="pl-c1">log</span>(<span class="pl-s"><span class="pl-pds">'</span>a.js 执行完毕<span
                class="pl-pds">'</span></span>);</pre>
    </div>
    <p>
        上面代码之中，<code>a.js</code>脚本先输出一个<code>done</code>变量，然后加载另一个脚本文件<code>b.js</code>。注意，此时<code>a.js</code>代码就停在这里，等待<code>b.js</code>执行完毕，再往下执行。
    </p>
    <p>再看<code>b.js</code>的代码。</p>
    <div class="highlight highlight-source-js"><pre><span class="pl-c1">exports</span>.<span class="pl-smi">done</span> <span
            class="pl-k">=</span> <span class="pl-c1">false</span>;
<span class="pl-k">var</span> a <span class="pl-k">=</span> <span class="pl-c1">require</span>(<span class="pl-s"><span
                class="pl-pds">'</span>./a.js<span class="pl-pds">'</span></span>);
<span class="pl-en">console</span>.<span class="pl-c1">log</span>(<span class="pl-s"><span class="pl-pds">'</span>在 b.js 之中，a.done = %j<span
                class="pl-pds">'</span></span>, <span class="pl-smi">a</span>.<span class="pl-smi">done</span>);
<span class="pl-c1">exports</span>.<span class="pl-smi">done</span> <span class="pl-k">=</span> <span
                class="pl-c1">true</span>;
<span class="pl-en">console</span>.<span class="pl-c1">log</span>(<span class="pl-s"><span class="pl-pds">'</span>b.js 执行完毕<span
                class="pl-pds">'</span></span>);</pre>
    </div>
    <p>
        上面代码之中，<code>b.js</code>执行到第二行，就会去加载<code>a.js</code>，这时，就发生了“循环加载”。系统会去<code>a.js</code>模块对应对象的<code>exports</code>属性取值，可是因为<code>a.js</code>还没有执行完，从<code>exports</code>属性只能取回已经执行的部分，而不是最后的值。
    </p>
    <p><code>a.js</code>已经执行的部分，只有一行。</p>
    <div class="highlight highlight-source-js">
        <pre><span class="pl-c1">exports</span>.<span class="pl-smi">done</span> <span class="pl-k">=</span> <span
                class="pl-c1">false</span>;</pre>
    </div>
    <p>因此，对于<code>b.js</code>来说，它从<code>a.js</code>只输入一个变量<code>done</code>，值为<code>false</code>。</p>
    <p>然后，<code>b.js</code>接着往下执行，等到全部执行完毕，再把执行权交还给<code>a.js</code>。于是，<code>a.js</code>接着往下执行，直到执行完毕。我们写一个脚本<code>main.js</code>，验证这个过程。
    </p>
    <div class="highlight highlight-source-js"><pre><span class="pl-k">var</span> a <span class="pl-k">=</span> <span
            class="pl-c1">require</span>(<span class="pl-s"><span class="pl-pds">'</span>./a.js<span
            class="pl-pds">'</span></span>);
<span class="pl-k">var</span> b <span class="pl-k">=</span> <span class="pl-c1">require</span>(<span class="pl-s"><span
                class="pl-pds">'</span>./b.js<span class="pl-pds">'</span></span>);
<span class="pl-en">console</span>.<span class="pl-c1">log</span>(<span class="pl-s"><span class="pl-pds">'</span>在 main.js 之中, a.done=%j, b.done=%j<span
                class="pl-pds">'</span></span>, <span class="pl-smi">a</span>.<span class="pl-smi">done</span>, <span
                class="pl-smi">b</span>.<span class="pl-smi">done</span>);</pre>
    </div>
    <p>执行<code>main.js</code>，运行结果如下。</p>
    <div class="highlight highlight-source-shell"><pre>$ node main.js

在 b.js 之中，a.done = <span class="pl-c1">false</span>
b.js 执行完毕
在 a.js 之中，b.done = <span class="pl-c1">true</span>
a.js 执行完毕
在 main.js 之中, a.done=true, b.done=true</pre>
    </div>
    <p>上面的代码证明了两件事。一是，在<code>b.js</code>之中，<code>a.js</code>没有执行完毕，只执行了第一行。二是，<code>main.js</code>执行到第二行时，不会再次执行<code>b.js</code>，而是输出缓存的<code>b.js</code>的执行结果，即它的第四行。
    </p>
    <div class="highlight highlight-source-js">
        <pre><span class="pl-c1">exports</span>.<span class="pl-smi">done</span> <span class="pl-k">=</span> <span
                class="pl-c1">true</span>;</pre>
    </div>
    <p>总之，CommonJS 输入的是被输出值的拷贝，不是引用。</p>
    <p>另外，由于 CommonJS 模块遇到循环加载时，返回的是当前已经执行的部分的值，而不是代码全部执行后的值，两者可能会有差异。所以，输入变量的时候，必须非常小心。</p>
    <div class="highlight highlight-source-js"><pre><span class="pl-k">var</span> a <span class="pl-k">=</span> <span
            class="pl-c1">require</span>(<span class="pl-s"><span class="pl-pds">'</span>a<span class="pl-pds">'</span></span>); <span
            class="pl-c"><span class="pl-c">//</span> 安全的写法</span>
<span class="pl-k">var</span> foo <span class="pl-k">=</span> <span class="pl-c1">require</span>(<span
                class="pl-s"><span class="pl-pds">'</span>a<span class="pl-pds">'</span></span>).<span class="pl-smi">foo</span>; <span
                class="pl-c"><span class="pl-c">//</span> 危险的写法</span>

<span class="pl-c1">exports</span>.<span class="pl-en">good</span> <span class="pl-k">=</span> <span class="pl-k">function</span> (<span
                class="pl-smi">arg</span>) {
  <span class="pl-k">return</span> <span class="pl-smi">a</span>.<span class="pl-en">foo</span>(<span class="pl-s"><span
                class="pl-pds">'</span>good<span class="pl-pds">'</span></span>, arg); <span class="pl-c"><span
                class="pl-c">//</span> 使用的是 a.foo 的最新值</span>
};

<span class="pl-c1">exports</span>.<span class="pl-en">bad</span> <span class="pl-k">=</span> <span class="pl-k">function</span> (<span
                class="pl-smi">arg</span>) {
  <span class="pl-k">return</span> <span class="pl-en">foo</span>(<span class="pl-s"><span
                class="pl-pds">'</span>bad<span class="pl-pds">'</span></span>, arg); <span class="pl-c"><span
                class="pl-c">//</span> 使用的是一个部分加载时的值</span>
};</pre>
    </div>
    <p>上面代码中，如果发生循环加载，<code>require('a').foo</code>的值很可能后面会被改写，改用<code>require('a')</code>会更保险一点。</p>
    <h3><a href="#es6-模块的循环加载"  class="anchor" id="user-content-es6-模块的循环加载">

    </a>ES6 模块的循环加载
    </h3>
    <p>ES6 处理“循环加载”与 CommonJS 有本质的不同。ES6 模块是动态引用，如果使用<code>import</code>从一个模块加载变量（即<code>import foo from 'foo'</code>），那些变量不会被缓存，而是成为一个指向被加载模块的引用，需要开发者自己保证，真正取值的时候能够取到值。
    </p>
    <p>请看下面这个例子。</p>
    <div class="highlight highlight-source-js"><pre><span class="pl-c"><span class="pl-c">//</span> a.mjs</span>
<span class="pl-k">import</span> {<span class="pl-smi">bar</span>} <span class="pl-k">from</span> <span
                class="pl-s"><span class="pl-pds">'</span>./b<span class="pl-pds">'</span></span>;
<span class="pl-en">console</span>.<span class="pl-c1">log</span>(<span class="pl-s"><span
                class="pl-pds">'</span>a.mjs<span class="pl-pds">'</span></span>);
<span class="pl-en">console</span>.<span class="pl-c1">log</span>(bar);
<span class="pl-k">export</span> <span class="pl-k">let</span> foo <span class="pl-k">=</span> <span class="pl-s"><span
                class="pl-pds">'</span>foo<span class="pl-pds">'</span></span>;

<span class="pl-c"><span class="pl-c">//</span> b.mjs</span>
<span class="pl-k">import</span> {<span class="pl-smi">foo</span>} <span class="pl-k">from</span> <span
                class="pl-s"><span class="pl-pds">'</span>./a<span class="pl-pds">'</span></span>;
<span class="pl-en">console</span>.<span class="pl-c1">log</span>(<span class="pl-s"><span
                class="pl-pds">'</span>b.mjs<span class="pl-pds">'</span></span>);
<span class="pl-en">console</span>.<span class="pl-c1">log</span>(foo);
<span class="pl-k">export</span> <span class="pl-k">let</span> bar <span class="pl-k">=</span> <span class="pl-s"><span
                class="pl-pds">'</span>bar<span class="pl-pds">'</span></span>;</pre>
    </div>
    <p>上面代码中，<code>a.mjs</code>加载<code>b.mjs</code>，<code>b.mjs</code>又加载<code>a.mjs</code>，构成循环加载。执行<code>a.mjs</code>，结果如下。
    </p>
    <div class="highlight highlight-source-shell"><pre>$ node --experimental-modules a.mjs
b.mjs
ReferenceError: foo is not defined</pre>
    </div>
    <p>上面代码中，执行<code>a.mjs</code>以后会报错，<code>foo</code>变量未定义，这是为什么？</p>
    <p>让我们一行行来看，ES6
        循环加载是怎么处理的。首先，执行<code>a.mjs</code>以后，引擎发现它加载了<code>b.mjs</code>，因此会优先执行<code>b.mjs</code>，然后再执行<code>a.js</code>。接着，执行<code>b.mjs</code>的时候，已知它从<code>a.mjs</code>输入了<code>foo</code>接口，这时不会去执行<code>a.mjs</code>，而是认为这个接口已经存在了，继续往下执行。执行到第三行<code>console.log(foo)</code>的时候，才发现这个接口根本没定义，因此报错。
    </p>
    <p>解决这个问题的方法，就是让<code>b.mjs</code>运行的时候，<code>foo</code>已经有定义了。这可以通过将<code>foo</code>写成函数来解决。</p>
    <div class="highlight highlight-source-js"><pre><span class="pl-c"><span class="pl-c">//</span> a.mjs</span>
<span class="pl-k">import</span> {<span class="pl-smi">bar</span>} <span class="pl-k">from</span> <span
                class="pl-s"><span class="pl-pds">'</span>./b<span class="pl-pds">'</span></span>;
<span class="pl-en">console</span>.<span class="pl-c1">log</span>(<span class="pl-s"><span
                class="pl-pds">'</span>a.mjs<span class="pl-pds">'</span></span>);
<span class="pl-en">console</span>.<span class="pl-c1">log</span>(<span class="pl-en">bar</span>());
<span class="pl-k">function</span> <span class="pl-en">foo</span>() { <span class="pl-k">return</span> <span
                class="pl-s"><span class="pl-pds">'</span>foo<span class="pl-pds">'</span></span> }
<span class="pl-k">export</span> {<span class="pl-smi">foo</span>};

<span class="pl-c"><span class="pl-c">//</span> b.mjs</span>
<span class="pl-k">import</span> {<span class="pl-smi">foo</span>} <span class="pl-k">from</span> <span
                class="pl-s"><span class="pl-pds">'</span>./a<span class="pl-pds">'</span></span>;
<span class="pl-en">console</span>.<span class="pl-c1">log</span>(<span class="pl-s"><span
                class="pl-pds">'</span>b.mjs<span class="pl-pds">'</span></span>);
<span class="pl-en">console</span>.<span class="pl-c1">log</span>(<span class="pl-en">foo</span>());
<span class="pl-k">function</span> <span class="pl-en">bar</span>() { <span class="pl-k">return</span> <span
                class="pl-s"><span class="pl-pds">'</span>bar<span class="pl-pds">'</span></span> }
<span class="pl-k">export</span> {<span class="pl-smi">bar</span>};</pre>
    </div>
    <p>这时再执行<code>a.mjs</code>就可以得到预期结果。</p>
    <div class="highlight highlight-source-shell"><pre>$ node --experimental-modules a.mjs
b.mjs
foo
a.mjs
bar</pre>
    </div>
    <p>这是因为函数具有提升作用，在执行<code>import {bar} from './b'</code>时，函数<code>foo</code>就已经有定义了，所以<code>b.mjs</code>加载的时候不会报错。这也意味着，如果把函数<code>foo</code>改写成函数表达式，也会报错。
    </p>
    <div class="highlight highlight-source-js"><pre><span class="pl-c"><span class="pl-c">//</span> a.mjs</span>
<span class="pl-k">import</span> {<span class="pl-smi">bar</span>} <span class="pl-k">from</span> <span
                class="pl-s"><span class="pl-pds">'</span>./b<span class="pl-pds">'</span></span>;
<span class="pl-en">console</span>.<span class="pl-c1">log</span>(<span class="pl-s"><span
                class="pl-pds">'</span>a.mjs<span class="pl-pds">'</span></span>);
<span class="pl-en">console</span>.<span class="pl-c1">log</span>(<span class="pl-en">bar</span>());
<span class="pl-k">const</span> <span class="pl-c1">foo</span> <span class="pl-k">=</span> () <span
                class="pl-k">=&gt;</span> <span class="pl-s"><span class="pl-pds">'</span>foo<span
                class="pl-pds">'</span></span>;
<span class="pl-k">export</span> {<span class="pl-smi">foo</span>};</pre>
    </div>
    <p>上面代码的第四行，改成了函数表达式，就不具有提升作用，执行就会报错。</p>
    <p>我们再来看 ES6 模块加载器<a
            href="https://github.com/ModuleLoader/es6-module-loader/blob/master/docs/circular-references-bindings.md">SystemJS</a>给出的一个例子。
    </p>
    <div class="highlight highlight-source-js"><pre><span class="pl-c"><span class="pl-c">//</span> even.js</span>
<span class="pl-k">import</span> { <span class="pl-smi">odd</span> } <span class="pl-k">from</span> <span
                class="pl-s"><span class="pl-pds">'</span>./odd<span class="pl-pds">'</span></span>
<span class="pl-k">export</span> <span class="pl-k">var</span> counter <span class="pl-k">=</span> <span
                class="pl-c1">0</span>;
<span class="pl-k">export</span> <span class="pl-k">function</span> <span class="pl-en">even</span>(<span
                class="pl-smi">n</span>) {
  counter<span class="pl-k">++</span>;
  <span class="pl-k">return</span> n <span class="pl-k">===</span> <span class="pl-c1">0</span> <span
                class="pl-k">||</span> <span class="pl-en">odd</span>(n <span class="pl-k">-</span> <span class="pl-c1">1</span>);
}

<span class="pl-c"><span class="pl-c">//</span> odd.js</span>
<span class="pl-k">import</span> { <span class="pl-smi">even</span> } <span class="pl-k">from</span> <span class="pl-s"><span
                class="pl-pds">'</span>./even<span class="pl-pds">'</span></span>;
<span class="pl-k">export</span> <span class="pl-k">function</span> <span class="pl-en">odd</span>(<span class="pl-smi">n</span>) {
  <span class="pl-k">return</span> n <span class="pl-k">!==</span> <span class="pl-c1">0</span> <span class="pl-k">&amp;&amp;</span> <span
                class="pl-en">even</span>(n <span class="pl-k">-</span> <span class="pl-c1">1</span>);
}</pre>
    </div>
    <p>上面代码中，<code>even.js</code>里面的函数<code>even</code>有一个参数<code>n</code>，只要不等于 0，就会减去 1，传入加载的<code>odd()</code>。<code>odd.js</code>也会做类似操作。
    </p>
    <p>运行上面这段代码，结果如下。</p>
    <div class="highlight highlight-source-js"><pre>$ babel<span class="pl-k">-</span>node
<span class="pl-k">&gt;</span> <span class="pl-k">import</span> <span class="pl-c1">*</span> <span
                class="pl-k">as</span> <span class="pl-smi">m</span> <span class="pl-k">from</span> <span
                class="pl-s"><span class="pl-pds">'</span>./even.js<span class="pl-pds">'</span></span>;
<span class="pl-k">&gt;</span> <span class="pl-smi">m</span>.<span class="pl-en">even</span>(<span
                class="pl-c1">10</span>);
<span class="pl-c1">true</span>
<span class="pl-k">&gt;</span> <span class="pl-smi">m</span>.<span class="pl-smi">counter</span>
<span class="pl-c1">6</span>
<span class="pl-k">&gt;</span> <span class="pl-smi">m</span>.<span class="pl-en">even</span>(<span
                class="pl-c1">20</span>)
<span class="pl-c1">true</span>
<span class="pl-k">&gt;</span> <span class="pl-smi">m</span>.<span class="pl-smi">counter</span>
<span class="pl-c1">17</span></pre>
    </div>
    <p>上面代码中，参数<code>n</code>从 10 变为 0 的过程中，<code>even()</code>一共会执行 6 次，所以变量<code>counter</code>等于
        6。第二次调用<code>even()</code>时，参数<code>n</code>从 20 变为 0，<code>even()</code>一共会执行 11 次，加上前面的 6
        次，所以变量<code>counter</code>等于 17。</p>
    <p>这个例子要是改写成 CommonJS，就根本无法执行，会报错。</p>
    <div class="highlight highlight-source-js"><pre><span class="pl-c"><span class="pl-c">//</span> even.js</span>
<span class="pl-k">var</span> odd <span class="pl-k">=</span> <span class="pl-c1">require</span>(<span
                class="pl-s"><span class="pl-pds">'</span>./odd<span class="pl-pds">'</span></span>);
<span class="pl-k">var</span> counter <span class="pl-k">=</span> <span class="pl-c1">0</span>;
<span class="pl-c1">exports</span>.<span class="pl-smi">counter</span> <span class="pl-k">=</span> counter;
<span class="pl-c1">exports</span>.<span class="pl-en">even</span> <span class="pl-k">=</span> <span class="pl-k">function</span> (<span
                class="pl-smi">n</span>) {
  counter<span class="pl-k">++</span>;
  <span class="pl-k">return</span> n <span class="pl-k">==</span> <span class="pl-c1">0</span> <span
                class="pl-k">||</span> <span class="pl-en">odd</span>(n <span class="pl-k">-</span> <span class="pl-c1">1</span>);
}

<span class="pl-c"><span class="pl-c">//</span> odd.js</span>
<span class="pl-k">var</span> even <span class="pl-k">=</span> <span class="pl-c1">require</span>(<span
                class="pl-s"><span class="pl-pds">'</span>./even<span class="pl-pds">'</span></span>).<span
                class="pl-smi">even</span>;
<span class="pl-c1">module</span>.<span class="pl-en">exports</span> <span class="pl-k">=</span> <span class="pl-k">function</span> (<span
                class="pl-smi">n</span>) {
  <span class="pl-k">return</span> n <span class="pl-k">!=</span> <span class="pl-c1">0</span> <span class="pl-k">&amp;&amp;</span> <span
                class="pl-en">even</span>(n <span class="pl-k">-</span> <span class="pl-c1">1</span>);
}</pre>
    </div>
    <p>上面代码中，<code>even.js</code>加载<code>odd.js</code>，而<code>odd.js</code>又去加载<code>even.js</code>，形成“循环加载”。这时，执行引擎就会输出<code>even.js</code>已经执行的部分（不存在任何结果），所以在<code>odd.js</code>之中，变量<code>even</code>等于<code>null</code>，等到后面调用<code>even(n-1)</code>就会报错。
    </p>
    <div class="highlight highlight-source-shell"><pre>$ node
<span class="pl-k">&gt;</span> var m = require(<span class="pl-s"><span class="pl-pds">'</span>./even<span
                class="pl-pds">'</span></span>)<span class="pl-k">;</span>
<span class="pl-k">&gt;</span> m.even(10)
TypeError: even is not a function</pre>
    </div>
    <h2><a href="#es6-模块的转码"  class="anchor" id="user-content-es6-模块的转码">

    </a>ES6 模块的转码
    </h2>
    <p>浏览器目前还不支持 ES6 模块，为了现在就能使用，可以将转为 ES5 的写法。除了 Babel 可以用来转码之外，还有以下两个方法，也可以用来转码。</p>
    <h3><a href="#es6-module-transpiler"  class="anchor" id="user-content-es6-module-transpiler">

    </a>ES6 module transpiler
    </h3>
    <p><a href="https://github.com/esnext/es6-module-transpiler">ES6 module transpiler</a>是 square 公司开源的一个转码器，可以将 ES6
        模块转为 CommonJS 模块或 AMD 模块的写法，从而在浏览器中使用。</p>
    <p>首先，安装这个转码器。</p>
    <div class="highlight highlight-source-shell">
        <pre>$ npm install -g es6-module-transpiler</pre>
    </div>
    <p>然后，使用<code>compile-modules convert</code>命令，将 ES6 模块文件转码。</p>
    <div class="highlight highlight-source-shell">
        <pre>$ compile-modules convert file1.js file2.js</pre>
    </div>
    <p><code>-o</code>参数可以指定转码后的文件名。</p>
    <div class="highlight highlight-source-shell">
        <pre>$ compile-modules convert -o out.js file1.js</pre>
    </div>
    <h3><a href="#systemjs"  class="anchor" id="user-content-systemjs">

    </a>SystemJS
    </h3>
    <p>另一种解决方法是使用 <a href="https://github.com/systemjs/systemjs">SystemJS</a>。它是一个垫片库（polyfill），可以在浏览器内加载 ES6 模块、AMD 模块和
        CommonJS 模块，将其转为 ES5 格式。它在后台调用的是 Google 的 Traceur 转码器。</p>
    <p>使用时，先在网页内载入<code>system.js</code>文件。</p>
    <div class="highlight highlight-text-html-basic">
        <pre>&lt;<span class="pl-ent">script</span> <span class="pl-e">src</span>=<span class="pl-s"><span
                class="pl-pds">"</span>system.js<span class="pl-pds">"</span></span>&gt;&lt;/<span
                class="pl-ent">script</span>&gt;</pre>
    </div>
    <p>然后，使用<code>System.import</code>方法加载模块文件。</p>
    <div class="highlight highlight-text-html-basic"><pre>&lt;<span class="pl-ent">script</span>&gt;<span
            class="pl-s1"></span>
<span class="pl-s1">  <span class="pl-smi">System</span>.<span class="pl-en">import</span>(<span class="pl-s"><span
        class="pl-pds">'</span>./app.js<span class="pl-pds">'</span></span>);</span>
<span class="pl-s1"></span><span class="pl-s1">&lt;</span>/<span class="pl-ent">script</span>&gt;</pre>
    </div>
    <p>上面代码中的<code>./app</code>，指的是当前目录下的 app.js 文件。它可以是 ES6 模块文件，<code>System.import</code>会自动将其转码。</p>
    <p>需要注意的是，<code>System.import</code>使用异步加载，返回一个 Promise 对象，可以针对这个对象编程。下面是一个模块文件。</p>
    <div class="highlight highlight-source-js"><pre><span class="pl-c"><span
            class="pl-c">//</span> app/es6-file.js:</span>

<span class="pl-k">export</span> <span class="pl-k">class</span> <span class="pl-en">q</span> {
  <span class="pl-en">constructor</span>() {
    <span class="pl-c1">this</span>.<span class="pl-smi">es6</span> <span class="pl-k">=</span> <span class="pl-s"><span
                class="pl-pds">'</span>hello<span class="pl-pds">'</span></span>;
  }
}</pre>
    </div>
    <p>然后，在网页内加载这个模块文件。</p>
    <div class="highlight highlight-text-html-basic"><pre>&lt;<span class="pl-ent">script</span>&gt;<span
            class="pl-s1"></span>
<span class="pl-s1"></span>
<span class="pl-s1"><span class="pl-smi">System</span>.<span class="pl-en">import</span>(<span class="pl-s"><span
        class="pl-pds">'</span>app/es6-file<span class="pl-pds">'</span></span>).<span class="pl-en">then</span>(<span
        class="pl-k">function</span>(<span class="pl-smi">m</span>) {</span>
<span class="pl-s1">  <span class="pl-en">console</span>.<span class="pl-c1">log</span>(<span
        class="pl-k">new</span> <span class="pl-en">m.q</span>().<span class="pl-smi">es6</span>); <span
        class="pl-c"><span class="pl-c">//</span> hello</span></span>
<span class="pl-s1">});</span>
<span class="pl-s1"></span>
<span class="pl-s1"></span><span class="pl-s1">&lt;</span>/<span class="pl-ent">script</span>&gt;</pre>
    </div>
    <p>上面代码中，<code>System.import</code>方法返回的是一个 Promise 对象，所以可以用<code>then</code>方法指定回调函数。</p>
</div>
</body>
</html>
